﻿using System;
using System.Globalization;
using System.Text;
using System.Xml.Serialization;
using Geo;
using Geo.Abstractions.Interfaces;
using Geo.Geodesy;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Fixpoint
    /// </summary>
    public class Fix : BaseEntity, ICloneable, IPosition
    {
        public static double LatLngDelta = 0.00001;
        public const int SctCol1 = 26;
        public const int SctCol = 15;
        public static readonly string SctCol1Str = "".PadRight(SctCol1);
        public static string KmlPlacemarkStyle = typeof (Fix).GetClassname();

        /// <summary>
        /// Latitude
        /// </summary>
        public double Latitude { get; set; }
        /// <summary>
        /// Longitude
        /// </summary>
        public double Longitude { get; set; }
        /// <summary>
        /// Latitude information as provided.
        /// </summary>
        public string LatitudeRaw { get; set; }
        /// <summary>
        /// Longitude information as provided.
        /// </summary>
        public string LongitudeRaw { get; set; }
        /// <summary>
        /// SCT coordinates (formatted string)
        /// </summary>
        [XmlIgnore]
        public string SctCoordinates { get { return this.GetSctCoordinates(); } }
        /// <summary>
        /// Altitude
        /// </summary>
        public Altitude AltitudeRestriction { get; set; }
        /// <summary>
        /// Calculated altitude
        /// </summary>
        public Altitude AltitudeCalculated { get; set; }
        /// <summary>
        /// Altitude
        /// </summary>
        [XmlIgnore]
        public Altitude Altitude { get { return this.AltitudeCalculated ?? this.AltitudeRestriction; } }
        /// <summary>
        /// Calculated fixpoint?
        /// </summary>
        public bool IsCalculated { get; set; }

        /// <summary>
        /// Fixpoint.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="lat"></param>
        /// <param name="lng"></param>
        /// <param name="canoncialName"></param>
        public Fix(string name, string canoncialName = null, string description = null, double lat = 0, double lng = 0) : base(name, canoncialName, description)
        {
            this.Longitude = lng;
            this.Latitude = lat;
            this.AltitudeRestriction = null;
            this.AltitudeCalculated = null;
            this.IsCalculated = false;
        }

        /// <summary>
        /// "Copy" constructor
        /// </summary>
        /// <param name="otherFix"></param>
        public Fix(Fix otherFix) : this(otherFix.Name, otherFix.CanonicalName, otherFix.Description, otherFix.Latitude, otherFix.Longitude)
        {
            if (otherFix == null) throw new ArgumentNullException("otherFix");
            if (otherFix.AltitudeRestriction != null) this.AltitudeRestriction = otherFix.AltitudeRestriction.Clone() as Altitude;
            this.IsCalculated = otherFix.IsCalculated;
        }

        /// <summary>
        /// Meaningful representation.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            if (String.IsNullOrEmpty(this.Name)) return base.ToString();
            string s = base.ToString() + " " + this.Latitude + "/" + this.Longitude;
            if (this.Altitude != null) s += " " + this.Altitude;
            return s;
        }

        /// <summary>
        /// Clone
        /// </summary>
        /// <returns></returns>
        public virtual object Clone()
        {
            return new Fix(this);
        }

        /// <summary>
        /// Is this fixpoint in blacklist?
        /// </summary>
        /// <param name="settings"></param>
        /// <returns></returns>
        public bool InBlacklist(GeneratorSettings settings)
        {
            if (settings == null || settings.FixBlacklist == null) return false;
            return (settings.FixBlacklist.Contains(this.Name));
        }

        /// <summary>
        /// Object based on Navigraph PMDG format.
        /// This is used to parse a single fixpoint on top of the SIDSTAR file, 
        /// not the fixpoint of a SID, STAR, TRANSITION. 
        /// </summary>
        /// <param name="fixLine"></param>
        /// <param name="replaceWithNavaid"></param>
        /// <returns></returns>
        /// <seealso cref="Procedure.FromPmdgFormat"/>
        public static Fix FixFromNavigraphPmdgFormat(string fixLine, bool replaceWithNavaid = false)
        {
            // FIX_AKINI LATLON N 48 44.98332 E 12 7.45002
            // FIX_D322D LATLON S 9 23.73552 E 147 10.818
            // 0   1     2      3 4  5       6  7   8
            //                  Deg / Min / Digits (not seconds)
            // http://andrew.hedges.name/experiments/convert_lat_long/
            if (String.IsNullOrWhiteSpace(fixLine)) return null;
            fixLine = fixLine.Trim();
            string[] parts = fixLine.Split(' ');
            if (parts.Length < 9) return null;
            if (parts[0] != "FIX") return null;
            if (parts[2] != "LATLON") return null;
            string name = parts[1];
            string latRaw = parts[3] + " " + parts[4] + " " + parts[5];
            string lngRaw = parts[6] + " " + parts[7] + " " + parts[8];
            double lat, latD, lng, lngD;
            if (!Double.TryParse(parts[5], NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out lat)) return null;
            if (!Double.TryParse(parts[8], NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out lng)) return null;
            if (!Double.TryParse(parts[4], NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out latD)) return null;
            if (!Double.TryParse(parts[7], NumberStyles.Float | NumberStyles.AllowThousands, Coordinates.ParseCulture, out lngD)) return null;
            const double min = 1.0/60;
            lat = lat*min + latD;
            lng = lng*min + lngD;
            if (parts[3] == "S") lat *= -1;
            if (parts[6] == "W") lng *= -1;
            Fix fix = new Fix(name) {Latitude = lat, LatitudeRaw = latRaw, Longitude = lng, LongitudeRaw = lngRaw};
            if (!replaceWithNavaid) return fix;
            fix = FixOnNavaidPosition.FindEquivalentNavaidDetails(fix);
            return fix;
        }

        /// <summary>
        /// Same position
        /// </summary>
        /// <param name="otherFix"></param>
        /// <returns></returns>
        public bool SamePosition(Fix otherFix)
        {
            if (otherFix == null) return false;
            if (Math.Abs(otherFix.Latitude - this.Latitude) > LatLngDelta) return false;
            return (Math.Abs(otherFix.Longitude - this.Longitude) <= LatLngDelta);
        }

        /// <summary>
        /// SCT coordinates
        /// </summary>
        /// <returns></returns>
        private string GetSctCoordinates()
        {
            StringBuilder sb = new StringBuilder();
            string c = (this.Latitude >= 0 ? "N" : "S") + Coordinates.LatLngDegToDegMinSecStr(this.Latitude);
            sb.Append(c.PadRight(SctCol));
            c = (this.Longitude >= 0 ? "E" : "W") + Coordinates.LatLngDegToDegMinSecStr(this.Longitude);
            sb.Append(c.PadRight(SctCol));
            return sb.ToString();
        }

        /// <summary>
        /// Convert to deg / min / sec / msec
        /// </summary>
        /// <param name="latLngDeg"></param>
        /// <param name="withMilliseconds"></param>
        /// <param name="deg"></param>
        /// <param name="min"></param>
        /// <param name="sec"></param>
        /// <param name="msec"></param>
        public static void LatLngDegToDegMinSec(double latLngDeg, bool withMilliseconds, out int deg, out int min, out int sec, out int msec)
        {
            msec = 0;
            double lld = Math.Abs(latLngDeg);
            deg = (int) (lld);
            double r = lld - deg;
            min = (int) (r*60);
            r = r - (min/60.0);
            double secr = r*3600.0;
            if (withMilliseconds) {
                sec = (int) secr;
                secr = secr - sec;
                msec = (int) Math.Round(secr*1000.0);
            }
            else {
                sec = (int) Math.Round(secr);
            }
            if (latLngDeg < 0) deg = -deg;
        }

        /// <summary>
        /// KML
        /// </summary>
        /// <param name="level"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public virtual string ToKml(int level, GeneratorSettings settings)
        {
            return Kml.Placemark(this.Name, this.Synopsis(), Fix.KmlPlacemarkStyle, this.Latitude, this.Longitude, level, false, settings);
        }

        /// <summary>
        /// Synopsis
        /// </summary>
        /// <returns></returns>
        public string Synopsis()
        {
            return (this.Altitude != null) ? this.Altitude.Synopsis() : "";
        }

        /// <summary>
        /// Coordinate for the Geo lib
        /// </summary>
        /// <returns></returns>
        public Coordinate GetCoordinate()
        {
            Coordinate coordinate = (this.AltitudeRestriction == null) ? new Coordinate(this.Latitude, this.Longitude) : new Coordinate(this.Latitude, this.Longitude, this.AltitudeRestriction.ValueM);
            return coordinate;
        }

        /// <summary>
        /// Distance
        /// </summary>
        /// <param name="otherFix"></param>
        /// <returns></returns>
        public double ShortestDistanceKm(Fix otherFix)
        {
            if (otherFix == null) throw new ArgumentNullException("otherFix");
            if (otherFix == this) return 0.0;
            GeodeticLine shortLine = this.CalculateShortestLine(otherFix);
            if (shortLine == null) return 0.0; // same position
            return shortLine.Distance.SiValue/1000.0; //km
        }
    }
}